Capítulo 1: un Padawan de Python

Del curso "Python y Machine Learning: de 0 a 100 con Reinforcement Learning"
Impartido por MalagaAI, [Andrés Matesanz][mate] y [Joaquín Terrasa][quim]
24 de Marzo de 2020

Duración estimada: 3 horas: 2 en streaming + 1 en trabajo individual.

Capítulo anterior (Introducción a Python): https://github.com/Matesanz/C0-intro-python

In [4]:
from IPython.display import YouTubeVideo
YouTubeVideo("tGsKzZtRwxw", width=100, height=100)
Out[4]:


DISCLAIMER: ¡Eres beta-tester de este curso! 😝 Si tienes sugerencias o ves fallos, por favor, comunicanoslo 💚

Este notebook está orientado a introducirte un poco más en la programación computacional - ¡ojito! Aunque lo centramos en Python, la mayoría de conceptos son aplicables a otros lenguajes para ciencia de datos (R, Julia, C++, etc.). El contenido se agrupa en 3 bloques principales, de los cuales resaltamos dos por estar estrictamente relacionados con la ciencia de datos:

  1. Mejor y más sencillo: cómo ser un maestro zen (de Python)
  2. Programación Funcional
  3. Computación numérica

Además, al final del notebook, así como en cada bloque, hay varios recursos para que podáis ir avanzando por vuestra cuenta. Antes de comenzar, hay 3 modos de obtener este notebook.

  • A través de GitHub, en el repositorio @espetro/pyCourse. Duplicas el repositorio (bien descargas el .zip o ejecutas git clone) y usas el notebook en local. Yo uso JupyterLab pero también podeis usar IPython.
  • Mediante Binder, un entorno de notebooks gratuito en la nube. Como alternative también lo tenéis en Google Colab.



Espero que todo funcione 🤞🤞




En la anterior clase pudimos iniciarnos con Python, el lenguaje más cool del último lustro. Supiste cómo instalar Python, qué es Anaconda y para qué puede sernos útil, y empezaste a programar en Python.

En este bloque, entenderás mejor por qué Python está partiendo la pana. Verás conocimientos fundamentales para cualquier desarrollador Python, y en concreto, iremos orientándonos a ser desarrollador Python para Machine Learning.

Como quizás ya sepas, Python no solo se usa para Machine Learning. Pero... ¿y por qué se usa?

Por qué usamos Python


Creo que este punto es importante porque, por un lado, te hace mejor programador, al razonar los motivos por los que programas en $X$ y no solo porque mi empresa o universidad trabaja con $X$; y por otro lado, puedes lograr a aprovechar las ventajas del lenguaje que usas.

  • Es un lenguaje enfocado en la legibilidad, trabajando con un gran subconjunto de palabras del inglés. De esta manera, proporciona un balance justo entre productividad y mantenimiento

    Fíjate! En Python no usamos corchetes {} para definir funciones ni bucles. Por defecto tampoco usamos tipos y además, se evita el uso de notación camelCase (miFuncion). Además, hay guias oficiales de cómo escribir código y documentación en Python 😌


  • Es un lenguaje multiparadigma, por lo que permite programar usando distintos modelos de programación, como el orientado a objetos o funcional. Además, es un lenguaje tipado dinámicamente, aunque deja abierta la posibilidad de tiparlo estáticamente.

    Fíjate! Al contrario que en Java o C, para guardar un número entero, no tienes que asignarle el tipo int. Por suerte, sí está fuertemente tipado, lo que hace que no puedas, por ejemplo, sumar 1 + "1", cosa que sí ocurre en JavaScript.
    En concreto, Python usa algo llamado duck typing: si se parece a un pato y hace 'quack' como un pato, entonces es un pato


  • Tiene una librería estándar robusta, lo que permite desarrollar nuevas librerias de manera más sencilla. Además, cuenta con una gran comunidad por detrás, lo que ayuda al mantenimiento y adaptación del lenguaje.

    Fíjate! El lenguaje es de código abierto, por lo que es la comunidad la que propone cambios.


  • Por último, tiene gran cabida en la gran mayoría de plataformas, como ya pudisteis comprobobar en el anterior capítulo 😉 Si bien es cierto que no es tan rapido como C o C++, la posibilidad de compilar en tiempo de ejecución y de poder ejecutar algunas tareas en C o C++ mejora bastante su rendimiento.

    Fíjate! Python es uno de los lenguajes base de las distribuciones Linux, estando presente también en MacOS.


Por ésto y por otras cosas, es porque está en varios rankings (PYPL, TIOBE) como uno de los lenguajes más populares.


Mejor y más sencillo: cómo ser un maestro zen (de Python)

En este bloque revisamos prácticas de programación en Python (aunque aplicables a otros lenguajes), con el fin de facilitar tu manejo con Python.

¿Por qué hay tantas versiones de Python? 🤯 ¿POR QUÉ? 😭😭😭😭

Las versiones de Python vienen a ser como la familia de Cletus de Los Simpsons:

cletus

  • Explicar las diferencias entre Python 2 y 3. Py2 perdió soporte este año!
  • Nos enfocamos en Python 3.x.
    • Principales características versiones 3.0 - 3.4.
    • En detalle: características de las versiones 3.5, 3.6, 3.7 y 3.8. Por qué elegimos la 3.7 como base.
      • 3.5: Corutinas asincronas, operador matricial @, tipado,
      • 3.6: Formateo con f"", separadores de millones con barrabaja 1_000_000, mejor implementacion dict
      • 3.7: Mejor concurrencia, Mejor tipado.
      • 3.8: Mejora varias funciones de las versiones anteriores y añade más complejidad al lenguaje 🙂

Ayuda

En Python, puedes usar help() sobre cualquier variable, palabra predefinida u objeto para obtener más información.

In [62]:
help(None)
Help on NoneType object:

class NoneType(object)
 |  Methods defined here:
 |  
 |  __bool__(self, /)
 |      self != 0
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.

Otro recurso útil a la hora de aprender es conocer las palabras reservadas de Python - ésto podemos revisarlo de manera sencilla, y nos servirá para aprender para qué se usa cada palabra:

In [2]:
import keyword

print(keyword.kwlist)
['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

También vale la pena conocer las funciones incluidas (built-in functions) en Python, las cuales puedes usar sin tener que importar ningún paquete:

In [25]:
import builtins
print(dir(builtins))
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError', '__IPYTHON__', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'display', 'divmod', 'enumerate', 'eval', 'exec', 'filter', 'float', 'format', 'frozenset', 'get_ipython', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

Por último pero no menos importante, habrá ocasiones donde necesites más funcionalidades que las que te da Python de entrada. Una de las mejores cosas de Python es su libreria estándar, la cual contiene (en la versión 3.7) un total de $211$ módulos, entre los que podemos encontrar:

  • re: para tratar con expresiones regulares.
  • datetime: para tratar con formatos de fecha y hora.
  • socket: para conexiones de red a bajo nivel.
  • random: para generar números aleatorios.
  • tkinter: para crear GUIs (graphical user interface).
  • os: para interactuar con el sistema. Contiene a su vez los módulos sys y path.
  • pickle: para serializar objetos.

Formateo de texto

Podemos formatear el texto en Python de varias maneras. Veamos cuáles hay:

In [2]:
print("Hoy es", 24, "de Marzo de", 2020)           # sintaxis clasica
print("Hoy es %d de Marzo de %d" % (24, 2020))     # sintaxis similar a C (printf, sprintf)
print("Hoy es" + " 24 " + "de Marzo de" + " 2020") # sintaxis similar a Java (System.out.print)
print("Hoy es {} de Marzo de {}".format(24, 2020))

dia, anyo = 24, 2020
print(f"Hoy es {dia} de Marzo de {anyo}")          # sintaxis similar a JS ES6 y Bash [solo disponible a partir de 3.6]

mi_dinero = 1_000_000
print(f"Tengo {mi_dinero} de € en el banco 🤑")
Hoy es 24 de Marzo de 2020
Hoy es 24 de Marzo de 2020
Hoy es 24 de Marzo de 2020
Hoy es 24 de Marzo de 2020
Hoy es 24 de Marzo de 2020
Tengo 1000000 de € en el banco 🤑

Tipado de datos

Síntesis de la clase anterior

¿Recuerdas los tipos de datos principales que dimos en el capítulo 0?

Todos estos tipos se pueden comprobar con la función type(). Además, hay que tener en cuenta otros tipos fundamentales:

  • ¿Conoces null? En Python no existe, pero tenemos None, cuyo tipo es NoneType.

  • Toda clase que creemos se considera un tipo nuevo. Por ello, toda instancia del objeto es del tipo de la clase.

    Si tenemos una clase Persona y una instancia/objeto pepe = Persona(edad=32, altura=180), type(pepe) es Persona

  • Además, en Python todas las funciones son objetos. Esto se conoce como First-Class Functions y permite, entre otras cosas, usar las variables como parámetros para las funciones, y almacenar las funciones en bases de datos.

Podemos ver todos los tipos aquí.

Cómo comprobar la equivalencia de tipos y datos en Python

Muy simple: en vez de usar ==, en Python podemos usar is (la cual también podemos usar para comprobar equivalencia de datos):

In [23]:
lista1 = [1, 3, "5", 7, "9", None]

filtrar_errores = [x for x in lista1 if x is not None]

sumar_dos = [x + 2 for x in filtrar_errores if type(x) is int]

print(filtrar_errores)
print(sumar_dos)
[1, 3, '5', 7, '9']
[3, 5, 9]
In [24]:
# tambien podemos usar "is" para
temp_var = 10 // 2
if temp_var is 5:
    print("Perfecto! No usaremos mas '=='")
Perfecto! No usaremos mas '=='
In [50]:
# Veamos los tipos de la clase Persona



class Persona:
    def __init__(self, edad, altura):
        self.edad = edad
        self.altura = altura
    
    def info(self):
        print(f"Mi edad es {self.edad} y mi altura es {self.altura}")
        
pepe = Persona(32, 180)
type(pepe)
Out[50]:
__main__.Persona
In [48]:
type(pepe.info)
Out[48]:
method
In [49]:
type(Persona.info)
Out[49]:
function
In [18]:
import re
type(re)
Out[18]:
module
In [16]:
type((x for x in range(10)))
Out[16]:
generator
In [46]:
type([x for x in range(10)])
Out[46]:
list
In [51]:
type(None)
Out[51]:
NoneType

Python ¿estáticamente tipado? 😱😱

Bueno... más o menos. Realmente Python permite añadir anotaciones de tipo. ¿Qué significa esto? Pues que sin una aplicación externa (como un plug-in de un IDE) no es posible aprovechar esta opción, pues el intérprete de Python no lo comprueba. Esta aplicación externa se ejecuta antes que el intérprete, y para la ejecución si encuentra algún conflicto de tipos, como ocurre en los lenguajes estáticamente tipados.

Esta posibilidad se introduce en Python 3.5 y se mejora en las siguientes versiones. Usaremos el módulo typing para ello.

In [12]:
from datetime import datetime as dtime

class Persona:
    duracion_anyo: int = 365
    
    def __init__(self, nombre: str, altura: float, fecha_de_nacimiento: str):
        self.nombre = nombre
        self.altura = altura
        self.fecha_de_nacimiento = dtime.strptime(fecha_de_nacimiento, "%Y-%m-%d")
        
    def wave(self):
        diferencia = dtime.now() - self.fecha_de_nacimiento
        edad_actual = diferencia.days // Persona.duracion_anyo
        
        print(f"Mi nombre es {self.nombre}, tengo {edad_actual} años y mido {self.altura} cms.")
In [13]:
# Necesitamos insertar la fecha en el formato "YYYY-MM-DD" ! 
Quino = Persona("Quino", 181, "1996-11-11")

Quino.wave()
Mi nombre es Quino, tengo 23 años y mido 181 cms.

Existen, además, tipos auxiliares como Union, que permiten dos tipos para una sola variable, y Optional, que permite que la variable sea de un tipo o None.

In [5]:
from typing import Any, Union, Optional, NewType

# Los tipos (para tipar) empiezan por mayusculas y si usan camelCase

variable_temporal: Any = 3.14 # acepta cualquier tipo. Similar a TypeScript, no?    

TextoHTTP = Optional[str]  # imagina que haces una peticion HTTP y no llega...¿que haces? Bien puedes capturar un error, u obtener un tipo 'None'

# Y si estas haciendo un clasificador de razas de gatos y perros, y quieres que te devuelva el tipo "Gato" o "Perro"?
TypeGato = NewType("Gato", str)
TypePerro = NewType("Perro", str)
ResultadoNN = Union[TypeGato, TypePerro]

variable_temporal = "hehe"

# A partir del 3.8 se pueden definir constantes (Final, como en JAVA)
# PI: Final[float] = 3.14

Crea código legible

Es lo que llaman el Zen de Python. Es uno de los primeros PEP creados y sintetiza las ideas para crear codigo legible.

Lo explícito es mejor que lo implícito.
Lo plano es mejor que lo enrevesado.
Los errores nunca deberían de ser silenciados.
Ahora es mejor que nunca.

Podemos subrayar varias convenciones:

  • Las clases y tipos definidos empiezan por mayúscula, y usan notación camelCase
  • Las variables y parámetros usan notacion_con_barras_bajas
  • Se incluye al menos una breve documentación """De este estilo""" para cada funcion
  • Para indicar variables protegidas usamos 1 barra baja: _variable_protegida
  • Para indicar variables privadas usamos 2 barras bajas: __variable_privada

Otro de los PEP fundamentales se centra en la guía de estilo para Python: el PEP8 es una de las guías fundamentales para estandarizar codigo en Python.

In [25]:
class Illo:
    def __init__(self):
        self.nombre = "Kino"
        self.__no_lo_sabras = True
        
var1 = Illo()
print(var1.nombre)
print(var1.__no_lo_sabras)
Kino
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-25-3cd2d344543d> in <module>
      6 var1 = Illo()
      7 print(var1.nombre)
----> 8 print(var1.__no_lo_sabras)

AttributeError: 'Illo' object has no attribute '__no_lo_sabras'

Generación de errores

Como hemos podido ver antes, para lanzar excepciones o errores en Python usamos la palabra raise, y podemos capturar o controlar los errores con try / except

In [153]:
raise Exception("boomboclat")
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-153-a906c8ce64f6> in <module>
----> 1 raise Exception("boomboclat")

Exception: boomboclat
In [152]:
try:
    raise Exception("boomboclat")
except Exception as e:
    print("No pasa nada mi gente, todo controlado")
    print(e)
No pasa nada mi gente, todo controlado
boomboclat

👉 más info 👈

Depuración de errores

La depuración de errores o debugging nos permite controlar momentáneamente el flujo del programa para detectar errores en tiempo de ejecución. De esta manera, podemos capturar y tratar errores que ni el compilador ni las herramientas externas nos permiten detectar.

  • En Python: Normalmente, corre a cargo del compilador, del IDE y de linters. A partir de la versión 3.7, se puede usar la función breakpoint().


    En la mayoría de IDEs o entornos de desarrollo, puedes marcar con un 🔴 en la barra lateral los puntos de interrupción o "breakpoints" cuando se ejecute el script

In [21]:
valor_externo = range(10)  # nos llega desde otro script o programa en red
valor_actual = [1.5, 2.0]

breakpoint()  # podemos leer y modificar datos durante la interrupcion
print(valor_actual + valor_externo)  # queremos unir listas. ¿se podra?
breakpoint()
--Return--
> <ipython-input-21-ceaa32e5db2a>(4)<module>()->None
-> breakpoint()
range(0, 10)
[1.5, 2.0]
No podremos :(
[1.5, 2.0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
--Call--
> c:\users\pachacho\miniconda3\envs\pycourse\lib\site-packages\ipython\core\displayhook.py(252)__call__()
-> def __call__(self, result=None):
  • En Jupyter, podemos usar el magic pdb, que activa la depuracion si y solo si un error ocurre. Tenemos que desactivarlo manualmente:
In [22]:
%pdb
Automatic pdb calling has been turned ON
In [23]:
# podemos leer y modificar datos durante la interrupcion
valor_externo = range(10)  # nos llega desde otro script o programa en red
valor_actual = [1.5, 2.0]

print(valor_actual + valor_externo)  # queremos unir listas. ¿se podra?
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-23-eccb4e3aff7d> in <module>
      3 valor_actual = [1.5, 2.0]
      4 
----> 5 print(valor_actual + valor_externo)  # queremos unir listas. ¿se podra?

TypeError: can only concatenate list (not "range") to list
None
> <ipython-input-23-eccb4e3aff7d>(5)<module>()
      1 # podemos leer y modificar datos durante la interrupcion
      2 valor_externo = range(10)  # nos llega desde otro script o programa en red
      3 valor_actual = [1.5, 2.0]
      4 
----> 5 print(valor_actual + valor_externo)  # queremos unir listas. ¿se podra?

Salta cuando algo da error
range(0, 10)
In [24]:
%pdb
Automatic pdb calling has been turned OFF

👉 más info 👈

Decoradores

Los decoradores son algo muy interesante, algo único de Python. Es algo así como las macros de Excel o Rust: permiten aplicar la funcionalidad de una clase o una función a uno o varios bloques del código, integrándose en la sintaxis del lenguaje y permitiendo una mejor lectura.

Podemos entender los decoradores como funciones de alto nivel, pues toman como argumento una función y devuelven otra función. Ésto lo veremos más adelante en el bloque funcional, pero quedate con ese concepto.

In [27]:
def agregar_barritas(funcion_con_texto):
    """Agrega barritas arriba y abajo de la funcion"""
    def envoltorio():
        """Aplica las barritas al texto"""
        print("==================")
        funcion_con_texto()
        print("==================")
        
    return envoltorio

def illo_que_ase():
    """Una funcion que produce texto en la consola"""
    print("Illo que ase")

con_barritas_todo_es_mejor = agregar_barritas(illo_que_ase)

illo_que_ase()
print("\n")
con_barritas_todo_es_mejor()
Illo que ase


==================
Illo que ase
==================

Lo realmente único de los decoradores es su azúcar sintáctico. ¡Puedes declarar el decorador sobre la funcion que escribe texto con una simple @!

In [28]:
@agregar_barritas
def illo_que_ase():
    """Una funcion que produce texto en la consola"""
    print("Illo que ase")
    
illo_que_ase()
==================
Illo que ase
==================

Los decoradores se usan mucho en paquetes de computación numérica. Por ejemplo, numba es una libreria que permite pre-compilar código Python a C/C++, y puede mejorar mucho código orientado a computación numérica. En este caso, usa decoradores @jit entre otros.

👉 más info sobre decoradores 👈

Estructuras por comprensión

  • Una manera directa de definir un diccionario es usar una sintaxis similar a la que usamos para listas por comprensión
In [29]:
keys = ["nombre", "altura", "esJoven"]
values = ["quino", 180, True]

dict1 = dict(nombre="quino", altura=180, esJoven=True)
dict2 = {"nombre": "quino", "altura": 180, "esJoven": True}  # al estilo JSON / JavaScript
dict3 = { key: value for (key, value) in zip(keys, values) }

print(dict3)
{'nombre': 'quino', 'altura': 180, 'esJoven': True}

Generadores

Algo fundamental para lenguajes funcionales como Haskell, son estructuras "pausadas", es decir, en las que todos sus valores no son calculados al crear la estructura. El ejemplo más claro está entre list y range:

In [31]:
lista = [0,1,2,3,4]
rango = range(0,5)

print("{} y {}".format(lista, rango))
[0, 1, 2, 3, 4] y range(0, 5)

Los generadores se definen sobre estructuras iterables, es decir, que se puedan recorrer. En cierto modo, cuando creas un iterador sobre una lista, obtienes un objeto Iterador sobre el que puedes usar una función next() para obtener el siguiente objeto en la estructura. Cuando la estructura llega a su fin, lanza un error StopIteration si ejecutas next() sobre el iterador ya vacío.

Hay dos formas de escribir generadores:

  • Si queremos crear generadores similares a listas, podemos usar una notación similar a la de por comprensión:
In [33]:
generador2 = (x ** 2 for x in [1,2,3])

print("{} {}".format(generador2, list(generador2)))
<generator object <genexpr> at 0x000001F3A7841448> [1, 4, 9]
  • Usando la palabra yield. De esta forma, podemos usarlo en cualquier tipo de estructura de datos iterable:
In [42]:
def obtener_proximo_elemento(lista):
    for indice, valor in enumerate(lista):
        yield (indice, valor)
        
iterador = obtener_proximo_elemento([1,2,3,4,5,6])
iterador
Out[42]:
<generator object obtener_proximo_elemento at 0x000001F3A78418C8>
In [43]:
print(f"{next(iterador)}, {next(iterador)}")

for i, v in iterador:
    print("Indice {} valor {}".format(i,v))
    
next(iterador)  # error!
(0, 1), (1, 2)
Indice 2 valor 3
Indice 3 valor 4
Indice 4 valor 5
Indice 5 valor 6
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-43-2ee230f3c694> in <module>
      4     print("Indice {} valor {}".format(i,v))
      5 
----> 6 next(iterador)  # error!

StopIteration: 

(Opcional) Documentación

Si quieres ir rápido, programa solo. Si quieres llegar lejos, programa con los demás

Tanto si es para desarrollar herramientas más complejas por ti mismo como si estás participando en un proyecto con otros desarrolladores, documentar adecuadamente es esencial. Python tiene su propia guía de estilo para la documentación, basada en reStructuredText y condensada en el PEP287 y PEP257. En Python, los llamamos docstrings.

De manera simple, reStructuredText es un lenguaje de texto enriquecido que compila a HTML. Veámoslo:

In [150]:
def saludar(nombre=None, entusiasmadamente=False):
        """Saluda a una persona, con fuerza o no.

        Parametros
        ----------
        nombre : str, obligatorio
            El nombre de la persona a saludar
        entusiasmadamente: bool, opcional
            Elige si saludar con fuerza o no

        Lanza
        ------
        TypeError
            Si el nombre es None
        """

        if nombre is None:
            raise TypeError("El nombre es None")

        exclamacion = ["", "!"][entusiasmadamente]  # equivalente a un operador ternario ?: (no recomendable! haz un 'if-else'. Esto no es tan legible)
            
        print("Hola Don Pepito! Hola Don {}{}".format(nombre, exclamacion))
        
saludar("Jose", True)
saludar("Jose", False)
Hola Don Pepito! Hola Don Jose!
Hola Don Pepito! Hola Don Jose

Fíjate! Una de las mejores maneras de documentar código en Python es asignar correctamente los nombres a funciones y variables temporales. Ten en cuenta el estilo de Python: estoNoEstaFino, esto_si_es_mejor

👉 más info 👈

(Opcional) Git / GitHub

Git es un software de control de versiones que permite mantener nuestro proyecto de forma sencilla. Se centra en una serie de comandos para darle una estructura en forma de árbol a nuestro proyecto, donde podemos guardar el progreso que hacemos, asi como volver hacia atrás o abrir nuevas ramas de desarrollo (para por ejemplo nuevas características).

En Machine Learning podemos aplicarlo para, por ejemplo, aplicar varias configuraciones a los hiperparámetros de nuestro modelo, sin que haya conflictos.

👉 más info 👈

(Opcional) Pruebas de Código

Este tema es algo más extenso, así que te daré una idea general. Realmente es algo que se hace a nivel de aplicación, cuando ya estás programando en un IDE (entorno de desarrollo), ya que en Jupyter no es del todo útil.

La idea fundamental consiste en probar todos los casos posibles de entrada y salida para tu programa, con el fin de comprobar si tu programa tiende a producir errores. Además, existe la posibilidad de simular el funcionamiento de tu programa incluso antes de haberlo programado, lo que nos ayuda a centrarnos en módulos específicos que necesitan más atención y cuidado.

  • Unit Testing: Se centra en comprobar que cada bloque (funciones, clases, etc.) funcionan como está previsto. Python incluye la libreria estándar unittest, así como la palabra reservada assert.

  • Test-Driven Development (TDD): es una metodología de programación que se centra en realizar primero los tests, para asegurar cómo debe funcionar el programa, para acto seguido implementarlo.

  • Mocking (Simulacion): permite simular módulos de tu app que aún no has desarrollado o sobre los que no tienes control. Un paquete bastante usado es mock.

  • Cobertura: facilita el comprobar si todos los bloques de código tiene pruebas de código.

👉 más info para mocking 👈

👉 más info para unit testing 👈

👉 más info para cobertura 👈

👉 más info para TDD 👈


2. Progamación funcional

La programación funcional es un paradigma de la programación, basada en el lambda calculus. Realmente ya has dado algo de programación funcional en Python, aunque no te hayas dado cuenta. Las listas por comprensión ([x for x in range(10)]) y la compilación perezosa de funciones es algo ímplicito en el lenguaje. Además, podemos ver otras características de lenguajes funcionales en Python.

Como antes comentamos, las funciones son ciudadanos de primera clase en Python. Ésto también se conoce como funciones de alto orden: virtualmente, toda función puede ser usada como parámetro de otra función o devolver otra función. Este último concepto, el de devolver otra función, también se conoce como funciones parciales, porque se aplican parcialmente.

Muchas de las utilidades funcionales están ya incluidas (built-in), en la libreria estándar functools o en itertools (para generar, por ejemplo, secuencias infinitas).

In [1]:
datos_json = {"nombre": "Jorge", "edad": 24, "altura": 180, "profesion": "marketing"}

datos_json["nombre"] #ok! pero y si...
datos_json["Nombre"]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-1-4118594dda04> in <module>
      2 
      3 datos_json["nombre"] #ok! pero y si...
----> 4 datos_json["Nombre"]

KeyError: 'Nombre'
In [6]:
datos_json.get("Nombre", "te has equivocao de clave :P")
Out[6]:
'te has equivocao de clave :P'

Funciones anónimas o lambda

Si has usado JavaScript, Haskell, Java 8+, ... conocerás las funciones anónimas. Éstas permiten definir una función de un solo uso en una sola línea. Digamos que las funciones que usamos, por ejemplo, para el map (mas1(valor)) solo la usamos ahí. ¿Por qué definir una función nueva, si simplemente puedes hacer...

In [38]:
list1 = [1,2,3]

# def mas1(valor):
#     return valor + 1

list1_mas1 = map(lambda valor: valor + 1, list1)
list(list1_mas1)
Out[38]:
[2, 3, 4]

👉 más info 👈

Map-Reduce

El lenguaje funcional permite una abstracción a la hora de modificar estructuras de datos. En vez de pensar cómo cambiar una estructura, como hacemos con un bucle for, pasamos a pensar qué queremos cambiar. Esto nos ahorra, por ejemplo, errores de acceso a memoria. Además, no tenemos que preocuparnos por iterar sobre la estructura (no usaremos más for, while, ... )

Vamos a dar 4 funciones fundamentales de los lenguajes funcionales:

  • map permite aplicar a cada elemento de una estructura, una función. Por ejemplo, podemos sumar 1 a todos los elementos de una lista:
In [14]:
list1 = [1,2,3]

def mas1(valor):
    return valor + 1

list(map(mas1, list1))
Out[14]:
[2, 3, 4]
  • filter permite seleccionar los elementos de una estructura que cumplan la función usada. Por ejemplo, podemos escoger solo los numeros pares de una lista:
In [16]:
def esPar(valor):
    return valor % 2 == 0

list(filter(esPar, list1))
Out[16]:
[2]
  • reduce permite reorganizar una estructura A para obtener otra estructura B. Normalmente, se usa para condensar estructuras complejas y conseguir estructuras mas simples, con valores acumulados. Por ejemplo, podemos sumar todos los elementos de una lista para obtener un valor total (una estructura más simple):
In [18]:
from functools import reduce

def sumar(valor1, valor2):
    # ojito! Ten en cuenta que, para 'reduce' el valor se acumula en el segundo (2do) valor
    return valor1 + valor2

reduce(sumar, list1)
Out[18]:
6
  • zip permite agrupar varias estructuras $A_x$ en una sola estructura $B$. La estructura $B$ está formada por tuplas de los elementos de las estructuras $A_x$. Por ejemplo, podemos agrupar los nombres y alturas de personas en una sola lista:
In [20]:
nombres = ["Quino", "Andres", "Fernando"]
altura = [181, 178, 180]

list(zip(nombres, altura))
Out[20]:
[('Quino', 181), ('Andres', 178), ('Fernando', 180)]

¿Por qué ocurre ésto? ¿Por qué hay que usar list? Respuesta: generadores (mira más abajo) 👇

In [21]:
print(map(mas1, lista1))
print(filter(esPar, lista1))
print(zip(nombres, altura))
<map object at 0x0000022FD5D08A08>
<filter object at 0x0000022FD5D085C8>
<zip object at 0x0000022FD5D089C8>

Inmutabilidad de datos

Los llamados lenguajes funcionales puros es un sub-paradigma que se centra en que el resultado de cualquier función dependa únicamente en los parámetros que le pasas, es decir, si ejecutas 2 veces una función con los mismos parámetros, ambas ejecuciones te devolverán lo mismo. Por ello, si usamos variables mutables, puede pasar esto:

from random import randint

def agrega_numero(lista):
    lista.append(randint(0, 1e4))
    return lista

x = list()
agrega_numero(x) # ¿Que devuelve?
agrega_numero(x) # ¿Que devuelve?

👆 ahí vemos que Python no es puramente funcional (ni queremos 😛).

Si ya conoces algun lenguaje con variables inmutables, sabrás que (en general) tienen mejor rendimiento que los lenguajes que no: Clojure, Haskell, Scala, Rust... Las ventajas de las estructuras inmutables, es que proporcionan paralelismo de datos, mayor seguridad y código más simple.

Algunas estructuras inmutables en Python son las tuplas, los sets "congelados" (*frozenset*), `range` y los tipos básicos (int, float, ...)

In [34]:
v1 = (1, True)
v1[0] = 3
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-34-8a53e3328d02> in <module>
      1 v1 = (1, True)
----> 2 v1[0] = 3

TypeError: 'tuple' object does not support item assignment
In [35]:
values = ("tomeu", 180, 25, "ingles c1")
my_var = frozenset(values)
my_var
Out[35]:
frozenset({180, 25, 'ingles c1', 'tomeu'})

👉 más info 👈

👉 ¿algo práctico? 👈

(Opcional) Lazy evaluation

Quizás ya te hayas preguntado por qué Python compila código que no es correcto, siempre que esté dentro de funciones o clases. Ésto es porque Python usa evaluación perezosa dentro de funciones. Esta funcionalidad no solo está en la evaluación de funciones, sino también en algo llamado generadores.

Básicamente, Python permite compilar una función cuando es declarada y solo comprobar si está correctamente definida cuando se ejecuta al menos una vez.

👉 más info 👈

In [43]:
def esto_no_hace_nada(illo):
    asdlkjqwelkj
In [44]:
esto_no_hace_nada(1)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-44-67ccf1db6a62> in <module>
----> 1 esto_no_hace_nada(1)

<ipython-input-43-97c2c1fc4799> in esto_no_hace_nada(illo)
      1 def esto_no_hace_nada(illo):
----> 2     asdlkjqwelkj

NameError: name 'asdlkjqwelkj' is not defined
In [30]:
generador_1 = range(10)
generador_2 = enumerate([10,20,30])
generador_3 = reversed([10,20,30])

print(generador_1, type(generador_1))
print(generador_2, type(generador_2), list(generador_2))
print(generador_3, type(generador_3), list(generador_3))
range(0, 10) <class 'range'>
<enumerate object at 0x000002365B9B9818> <class 'enumerate'> [(0, 10), (1, 20), (2, 30)]
<list_reverseiterator object at 0x000002365BE53248> <class 'list_reverseiterator'> [30, 20, 10]

(Opcional) Funciones parciales

La idea principal es que toda función $A$ con $N$ parámetros devuelve otra función $B$ con $N-1$ parámetros, donde $B$ tiene un estado interno diferente a $A$. Ésto quiere decir que, al igual que las clases, las funciones pueden mantener un estado interno (como la referencia self en las clases!). Un ejemplo clásico es el cálculo del número n de fibonacci:

$f_{0}=0\,$

$f_{1}=1\,$

$f_n = f_{n-1} + f_{n-2}$

In [17]:
def configurar_fib():
    a, b = 0, 1  # mantenemos un estado interno en la función. Esto se conoce como "memoization"
    def fib(n):
        if n == 0:
            return a
        elif n == 1:
            return b
        else:
            return fib(n-1) + fib(n-2)
    return fib
        
fib = configurar_fib()

[fib(x) for x in range(13)]
Out[17]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]

Existen dos maneras de crear funciones parciales en Python. La primera es hacerlo explícitamente:

In [8]:
def sumar_cantidad(cantidad):
    def sumar(otro_elemento):
        return otro_elemento + cantidad
    
    return sumar

sumar_dos = sumar_cantidad(2) # creas una funcion aplicada parcialmente

print(f"2 + 4 = {sumar_dos(4)} | 10 + 2 = {sumar_dos(10)}")
2 + 4 = 6 | 10 + 2 = 12

Otra manera es usar el método partial:

In [9]:
from functools import partial

def sumar(a, b):
    return a + b

sumar_dos = partial(sumar, b=2)  # hay que nombrar los parametros que vamos a "sobrescribir"

print(f"2 + 4 = {sumar_dos(4)} | 10 + 2 = {sumar_dos(10)}")
2 + 4 = 6 | 10 + 2 = 12

Existen, además, conceptos relacionados, como la currificación o las clausuras.

👉 más info sobre funciones parciales 👈

👉 más info sobre memoization 👈


3. Computación Numérica

Python es muy popular para Machine Learning. Algunas de las librerias más conocidas son numpy, pandas, matplotlib, scikit-learn, scipy, y algunas orientadas a redes neuronales, como son tensorflow o pytorch.

En este notebook veremos dos paquetes esenciales: numpy y pandas.

Numpy

Numpy es el paquete de computación numérica más usado de Python. Proporciona una infinidad de estructuras de datos y funciones que permiten el manejo masivo de datos. Es rápido gracias a que ejecuta C por debajo (como si no 😉), y permite realizar operaciones entre conjuntos grandes de datos, con un estilo similar a lo que se puede hacer en Matlab y R.

Veamos un ejemplo:

In [45]:
lista1 = [1,2,3]
lista2 = [0,0,0]

lista1 + lista2
Out[45]:
[1, 2, 3, 0, 0, 0]

No era lo esperado, ¿no?

En numpy, la estructura básica es el array, que es el "equivalente" a la lista en Python. Al igual que una lista, puede tener 1 o más dimensiones y puede tener varios tipos de datos alojados, aunque lo más frecuente es que solo tenga un tipo.

Aún así, la estructura más común es el nd-array o array multidimensional, pues permite almacenar datos en varios niveles de organización. De hecho, los arrays multidimensionales son inmutables.

In [48]:
import numpy as np

lista1 = np.array([1,2,3])
lista2 = np.array([0,0,0])

lista1 + lista2
Out[48]:
array([1, 2, 3])

La mayoría de funciones para crear estructuras de datos, necesitan conocer la forma de la matriz que queremos generar. Entendemos que una lista de N valores es una matriz $1 \times N$ o bien $N \times 1$

In [59]:
matriz_cuadrada = (4,4)  # siempre se usan tuplas!

ceros = np.zeros(matriz_cuadrada)
matriz_identidad = np.identity(4)  # las matrices identidades siempre son cuadradas

print(matriz_identidad + ceros)
[[1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [0. 0. 1. 0.]
 [0. 0. 0. 1.]]
In [56]:
np.identity(4)
Out[56]:
array([[1., 0., 0., 0.],
       [0., 1., 0., 0.],
       [0., 0., 1., 0.],
       [0., 0., 0., 1.]])
In [68]:
np.empty((3, 3))
Out[68]:
array([[0.00000000e+000, 0.00000000e+000, 0.00000000e+000],
       [0.00000000e+000, 0.00000000e+000, 7.13430793e-321],
       [8.70018274e-313, 2.31297541e-312, 0.00000000e+000]])
In [70]:
np.ones((2,2))  # fijate en los parentesis de la tupla!
# np.ones(2,2)  # da error!
Out[70]:
array([[1., 1.],
       [1., 1.]])
In [93]:
arr0 = np.array([0.2, 0.4, 0.6])
print(f"arr0.shape = {arr0.shape}")  # por que??

arr0.shape = (3, 1)

print("\n", arr0)
print("\n", arr0.transpose())
arr0.shape = (3,)

 [[0.2]
 [0.4]
 [0.6]]

 [[0.2 0.4 0.6]]
In [74]:
arr1 = np.array([1,2,3])
arr2 = np.array([4,5,6])

print(f"[arr1; arr2] = \n{np.vstack([arr1, arr2])}")
[arr1; arr2] = 
[[1 2 3]
 [4 5 6]]
In [101]:
arr3 = np.array([1,2,3])
arr3.shape = (3, 1)
arr4 = arr3 + .5

print(f"[arr3 arr4] = \n{np.hstack([arr3, arr4])}")
[arr3 arr4] = 
[[1.  1.5]
 [2.  2.5]
 [3.  3.5]]
In [102]:
np.mean(arr1)
Out[102]:
2.0
In [111]:
print(f"sum({list1}) = {sum(list1)}")
print(f"np.sum({arr1.transpose()}) = {np.sum(arr1)}")
sum([1, 2, 3]) = 6
np.sum([[1 2 3]]) = 6


Una característica fundamental de numpy es el broadcasting o "propagación", que asegura la posibilidad de realizar operaciones aritmeticas (suma o multiplicacion) entre matrices que, bien no tienen las mismas dimensiones, pero son compatibles. Veámoslo:

Si quieres saber más de numpy, te recomiendo el siguiente enlace:

👉 más info 👈

Pandas

pandas es la libreria estrella cuando hablamos de ciencia de datos en Python. En cierto modo, trata de traer a Python las tablas de R o data.frames (df pa' los amigos), algo así como una matriz con esteroides.


pandas permite leer y modificar conjuntos de datos (datasets) de manera sencilla, usando una sintaxis similar a numpy para el manejo de matrices. Además, proporciona utilidades para el manejo de ficheros, asi como para detectar datos "vacíos" y calcular valores acumulados.


In [112]:
import pandas as pd
import seaborn as sns  # ejecuta la cápsula de abajo y vuelve a ejecutar ésto!
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
<ipython-input-112-61e0adcaee13> in <module>
      1 import pandas as pd
----> 2 import seaborn as sns

ModuleNotFoundError: No module named 'seaborn'
In [113]:
# si escribes "!" al principio de la linea, puedes llamar a programas y comandos del shell / linea de comandos (ya sea shell, bash, cmd o Powershell)
!pip install seaborn
Collecting seaborn
  Downloading seaborn-0.10.0-py3-none-any.whl (215 kB)
Requirement already satisfied: pandas>=0.22.0 in c:\users\pachacho\miniconda3\envs\pycourse\lib\site-packages (from seaborn) (1.0.2)
Collecting scipy>=1.0.1
  Downloading scipy-1.4.1-cp37-cp37m-win_amd64.whl (30.9 MB)
Requirement already satisfied: numpy>=1.13.3 in c:\users\pachacho\miniconda3\envs\pycourse\lib\site-packages (from seaborn) (1.18.2)
Collecting matplotlib>=2.1.2
  Downloading matplotlib-3.2.1-cp37-cp37m-win_amd64.whl (9.2 MB)
Requirement already satisfied: python-dateutil>=2.6.1 in c:\users\pachacho\miniconda3\envs\pycourse\lib\site-packages (from pandas>=0.22.0->seaborn) (2.8.1)
Requirement already satisfied: pytz>=2017.2 in c:\users\pachacho\miniconda3\envs\pycourse\lib\site-packages (from pandas>=0.22.0->seaborn) (2019.3)
Collecting cycler>=0.10
  Downloading cycler-0.10.0-py2.py3-none-any.whl (6.5 kB)
Collecting kiwisolver>=1.0.1
  Downloading kiwisolver-1.1.0-cp37-none-win_amd64.whl (57 kB)
Collecting pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1
  Downloading pyparsing-2.4.6-py2.py3-none-any.whl (67 kB)
Requirement already satisfied: six>=1.5 in c:\users\pachacho\miniconda3\envs\pycourse\lib\site-packages (from python-dateutil>=2.6.1->pandas>=0.22.0->seaborn) (1.14.0)
Requirement already satisfied: setuptools in c:\users\pachacho\miniconda3\envs\pycourse\lib\site-packages (from kiwisolver>=1.0.1->matplotlib>=2.1.2->seaborn) (46.0.0.post20200309)
Installing collected packages: scipy, cycler, kiwisolver, pyparsing, matplotlib, seaborn
Successfully installed cycler-0.10.0 kiwisolver-1.1.0 matplotlib-3.2.1 pyparsing-2.4.6 scipy-1.4.1 seaborn-0.10.0
In [115]:
import pandas as pd
import seaborn as sns

# otra manera es cargarlo directamente
# iris = pd.read_csv('https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv')
iris = sns.load_dataset('iris')
iris.head()
Out[115]:
sepal_length sepal_width petal_length petal_width species
0 5.1 3.5 1.4 0.2 setosa
1 4.9 3.0 1.4 0.2 setosa
2 4.7 3.2 1.3 0.2 setosa
3 4.6 3.1 1.5 0.2 setosa
4 5.0 3.6 1.4 0.2 setosa
In [121]:
print(f" Tipo: {type(iris)}\n Columnas: {list(iris.columns)}\n")
 Tipo: <class 'pandas.core.frame.DataFrame'>
 Columnas: ['sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species']

In [129]:
iris[1:4] # seleccionar una o mas filas [inicio, final)
Out[129]:
sepal_length sepal_width petal_length petal_width species
1 4.9 3.0 1.4 0.2 setosa
2 4.7 3.2 1.3 0.2 setosa
3 4.6 3.1 1.5 0.2 setosa
In [130]:
iris["species"] # seleccionar columna
Out[130]:
0         setosa
1         setosa
2         setosa
3         setosa
4         setosa
         ...    
145    virginica
146    virginica
147    virginica
148    virginica
149    virginica
Name: species, Length: 150, dtype: object
In [131]:
iris.loc[1:4, "species"]  # seleccionar un subconjunto del dataframe [inicio, final]
Out[131]:
1    setosa
2    setosa
3    setosa
4    setosa
Name: species, dtype: object
In [132]:
iris.iloc[1:4, 4]  # seleccionar un subconjunto del dataframe, escogiendo la columna por su posicion [inicio, final)
Out[132]:
1    setosa
2    setosa
3    setosa
Name: species, dtype: object

Fíjate! Si al final del comentario ves [inicio, final), es porque el ultimo indice no se extrae (ej. 1:4 solo extrae las filas 1,2,3)

In [138]:
iris.shape
Out[138]:
(150, 5)
In [140]:
iris[iris["species"] == "virginica"].head(3)
Out[140]:
sepal_length sepal_width petal_length petal_width species
100 6.3 3.3 6.0 2.5 virginica
101 5.8 2.7 5.1 1.9 virginica
102 7.1 3.0 5.9 2.1 virginica
In [141]:
iris.dropna().shape  # por suerte este dataset nos lo dan limpito :)
Out[141]:
(150, 5)
In [142]:
iris.groupby("species").mean()
Out[142]:
sepal_length sepal_width petal_length petal_width
species
setosa 5.006 3.428 1.462 0.246
versicolor 5.936 2.770 4.260 1.326
virginica 6.588 2.974 5.552 2.026
In [143]:
iris["sepia_Length"] = iris["sepal_length"] / iris["sepal_width"]
iris.head(3)
Out[143]:
sepal_length sepal_width petal_length petal_width species sepia_Length
0 5.1 3.5 1.4 0.2 setosa 1.457143
1 4.9 3.0 1.4 0.2 setosa 1.633333
2 4.7 3.2 1.3 0.2 setosa 1.468750

Fijate que pandas aplica conceptos funcionales, como la inmutabilidad de datos al modificar dataframes 👇

In [145]:
iris.rename(columns = { "sepia_Length": "borra_esta" }, inplace=True)  # ojito! Algunas funciones devuelven una copia del df modificado
iris.head(3)
Out[145]:
sepal_length sepal_width petal_length petal_width species borra_esta
0 5.1 3.5 1.4 0.2 setosa 1.457143
1 4.9 3.0 1.4 0.2 setosa 1.633333
2 4.7 3.2 1.3 0.2 setosa 1.468750

Ten en cuenta que, al igual que con el manejo de BBDD, una función poco eficiente para filtrar varias tablas puede significar 2h más esperando.

De cara al tratamiento de datos, hay una forma bastante popular en la comunidad de R llamada tidy data ("datos organizados"). Esta metodología se centra en usar un cjto. de verbos para referirnos a la modificación de datos (algo parecido a prog. funcional).

👉 más info para aprender más de tidy data 👈

👉 más info para aprender más de pandas 👈

(Opcional) Visualización de datos

Normalmente en Python se usa matplotlib, una libreria para visualizacion de datos cientificos. Aun asi, yo no soy muy fan, pues es muy similar a la forma de dibujar graficos de MatLab 🙂 Aquí hay algunas alternativas:

  • Plotly, una libreria multiplataforma, que permite crear graficos interactivos (usa JS por detras jsjs). Además, permite interoperabilidad entre Python, R y Julia 🤩
  • Si habeis programado en R, conocereis ggplot2. En Python tienes plotnine y ggpy, que usan casi la misma API.
  • Bokeh es una alternativa muy similar a Plotly, que se centra en la Gramática de Gráficos, al igual que ggplot2 en R.
  • Altair Vega tambien usa una sintaxis modular para componer graficos. Muy usado en Scala, pues permite aplicar el estilo de Map-Reduce.

Vamos a ver cómo se usan matplotlib y plotly en Python. Para ello, usaremos el dataset iris o bien datos aleatorios.

Matplotlib

Es, con diferencia, la libreria más usada para DataViz en Python

In [21]:
%matplotlib inline  # realmente solo es necesario hacerlo una vez (al menos, en JupyterLab)

from matplotlib import pyplot as plt
from numpy.random import randn

ts = pd.Series(randn(1000), index=pd.date_range('1/1/2019', periods=1000))

ts = ts.cumsum()

ts.plot()
Out[21]:
<matplotlib.axes._subplots.AxesSubplot at 0x23b611797c8>
In [22]:
plt.figure()
ts.plot(style='k--', label='Series')
plt.legend()
Out[22]:
<matplotlib.legend.Legend at 0x23b61132ac8>
In [24]:
df = pd.DataFrame(np.random.rand(10, 4), columns=['a', 'b', 'c', 'd'])

df.plot(kind='bar');
In [26]:
df.plot(kind='bar', stacked=True)
In [27]:
iris.head(1)
Out[27]:
sepal_length sepal_width petal_length petal_width species
0 5.1 3.5 1.4 0.2 setosa
In [34]:
%matplotlib inline

plt.figure()
plt.hist(iris["sepal_length"], bins=15)
plt.xlabel("Longitud del sépalo")
plt.ylabel("Individuos")
plt.show()

matplotlib facilita además visualizaciones complejas, como la de *Coordenadas Paralelas*, que permite visualizar datos multidimensionales, permitiendo ver los clústers de datos (en este caso, respecto a una variable categórica *'species'*) a lo largo de varias variables cuantitativas.

In [40]:
from pandas.plotting import parallel_coordinates

plt.figure()
parallel_coordinates(iris, 'species', colormap='winter')
plt.show()


👉 más info para aprender más de matplotlib 👈

Plotly

Si quieres realizar visualizaciones interactivas, plotly es una de las librerias más usadas. Permite infinidad de configuraciones, y es extensible por definición, aunque usando JavaScript. Asimismo, tiene equivalentes en R y JavaScript. Plotly además incluye Plotly Express: An easy-to-use, high-level interface to Plotly, which operates on "tidy" data and produces easy-to-style figures.

Una de sus desventajas, al igual que pasa con otras librerias de visualizacion interactivas, es que no cuentan con gran soporte dentro de Jupyter o similares, por lo que siempre tienes que saber algunos trucos - de hecho, en un primer momento, matplotlib tampoco tenia un buen soporte y era confuso obtener la salida dentro de Jupyter.

In [7]:
# !pip install plotly "ipywidgets==7.5"  # valido para jupyter-notebooks
# para jupyter-lab es recomendable mirar la documentacion de Plotly, pues es mas enrevesado
In [2]:
import plotly.io as pio
pio.renderers
Out[2]:
Renderers configuration
-----------------------
    Default renderer: 'plotly_mimetype+notebook'
    Available renderers:
        ['plotly_mimetype', 'jupyterlab', 'nteract', 'vscode',
         'notebook', 'notebook_connected', 'kaggle', 'azure', 'colab',
         'cocalc', 'databricks', 'json', 'png', 'jpeg', 'jpg', 'svg',
         'pdf', 'browser', 'firefox', 'chrome', 'chromium', 'iframe',
         'iframe_connected', 'sphinx_gallery']
In [13]:
pio.renderers.default = "jupyterlab"
In [16]:
import plotly.graph_objects as go

fig = go.Figure(
    data=[go.Bar(y=[2, 1, 3])],
    layout_title_text="A Figure Displayed with fig.show()"
)

fig.show(renderer="browser")
In [1]:
import plotly.graph_objects as go

fig = go.Figure(
    data=[go.Bar(y=[2, 1, 3])],
    layout_title_text="A Figure Displayed with fig.show()"
)

# necesita las siguientes librerias para: jpg/svg/png
# !pip install requests psutil
# conda install -c plotly plotly-orca -y
fig.show(renderer="jpg")
In [12]:
import plotly.graph_objects as go

fig = go.Figure(
    data=[go.Bar(y=[2, 1, 3])],
    layout_title_text="A Figure Displayed with fig.show()"
)

fig.show(renderer="jupyterlab")
In [2]:
import plotly.graph_objects as go
import numpy as np

x = np.arange(10)

fig = go.Figure(data=go.Scatter(x=x, y=x**2))
fig.show()
In [3]:
import plotly.express as px

df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species")
fig.show()
In [53]:
import plotly.express as px
df = px.data.gapminder().query("year == 2007")
fig = px.line_geo(df, locations="iso_alpha",
                  color="continent", # "continent" is one of the columns of gapminder
                  projection="orthographic")
fig.show()
In [17]:
import plotly.express as px

df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species", marginal_y="rug", marginal_x="histogram")
fig
In [4]:
import plotly.express as px

df = px.data.gapminder()
fig = px.scatter(df, x="gdpPercap", y="lifeExp", animation_frame="year", animation_group="country",
           size="pop", color="continent", hover_name="country", facet_col="continent",
           log_x=True, size_max=45, range_x=[100,100000], range_y=[25,90])
fig.show()


👉 más info para aprender más de plotly 👈

👉 más info para aprender más de plotly-express 👈

👉 más info para aprender más de la integración de Plotly con Jupyter Notebook / Lab 👈 (bokeh tambien sigue un proceso similar)

(Opcional) Bases de datos abiertas

Una de las formas más sencillas y prácticas de aprender y entrenar tu habilidad con Python en el análisis de datos es aprovechar el open data o la iniciativa de datos abiertos. Desde datos recolectados por empresas u organizaciones, hasta datos generados continuamente por los organismos públicos:

  1. Uno de los repositorios de datos más populares en este campo es el de la Universidad de California (UCI), con 497 datasets de múltiples tipos de datos.
  2. Además, puedes aprovechar los datos generados por las administraciones públicas de nuestro país. En un ránking nacional, el portal de datos abiertos de Fuengirola es uno de los más transparentes y sencillos de cara al ciudadano
In [1]:
import pandas as pd

# info: https://datosabiertos.malaga.eu/dataset/padron-de-habitantes-por-distrito-y-seccion-censal-2019/resource/24ff297b-e7de-4c35-9477-3f45917bb4b3
padron_malaga_2019 = pd.read_csv("https://datosabiertos.malaga.eu/recursos/demografia/padron/2019/padrondistritosysecciones.csv")  # si tarda en cargar, agregar ', chunksize=10_000'
In [12]:
padron_malaga_2019.head()
Out[12]:
NHOP EDAD SEXO CPRON CMUNN NACI DIST SECC
0 542 0 6 29 67 108 7 68
1 1011 0 1 29 67 108 7 68
2 1227 0 1 29 67 407 7 37
3 1134 0 1 29 67 228 7 37
4 148 39 6 29 69 108 7 68
In [7]:
print(padron_malaga_2019.dtypes, "\n\n", padron_malaga_2019.columns)
NHOP     int64
EDAD     int64
SEXO     int64
CPRON    int64
CMUNN    int64
NACI     int64
DIST     int64
SECC     int64
dtype: object 

 Index(['NHOP', 'EDAD', 'SEXO', 'CPRON', 'CMUNN', 'NACI', 'DIST', 'SECC'], dtype='object')
In [8]:
codigos_municipales = pd.read_csv("http://datosabiertos.malaga.eu/recursos/demografia/padron/tablas-catalogo/municipio.csv")
codigos_municipales.head()
Out[8]:
PROV PROVINCIA MUN MUNICIPO FALTA FBAJA
0 1 Araba/Álava 1 Alegría-Dulantzi 18331130 99999990
1 1 Araba/Álava 2 Amurrio 18331130 99999990
2 1 Araba/Álava 3 Aramaio 18331130 99999990
3 1 Araba/Álava 4 Artziniega 18331130 99999990
4 1 Araba/Álava 6 Armiñón 18331130 99999990
In [11]:
codigos_municipales[codigos_municipales["MUNICIPO"] == "Málaga"]
Out[11]:
PROV PROVINCIA MUN MUNICIPO FALTA FBAJA
4610 29 Málaga 67 Málaga 18331130 99999990
In [34]:
print("ojito! Pandas usa el 'AND a nivel de bit' (bitwise) en vez del 'AND logico'")
print("AND logico: and, AND a nivel de bit: &\n\n")

# usamos un filtro booleano (muy eficiente)
filtro = (padron_malaga_2019["CMUNN"] == 67) & (padron_malaga_2019["SEXO"] == 6) & (padron_malaga_2019["EDAD"] > 40)
ciudadanas_femeninas_mayor40_malaga = padron_malaga_2019[filtro]

print("Total:", len(ciudadanas_femeninas_mayor40_malaga))
ciudadanas_femeninas_mayor40_malaga.head(3)
ojito! Pandas usa el 'AND a nivel de bit' (bitwise) en vez del 'AND logico'
AND logico: and, AND a nivel de bit: &


Total: 90657
Out[34]:
NHOP EDAD SEXO CPRON CMUNN NACI DIST SECC
155 471 71 6 29 67 108 6 42
156 34 71 6 29 67 108 6 42
157 24 68 6 29 67 108 6 42
In [31]:
 
Out[31]:
90657

Y de aquí.. ¿a Pekín?

Creo que el contenido del capítulo ya es suficientemente denso como para seguir avanzando, así que dejo unos cuantos recursos por si quereis seguir avanzando:

Extras

  • Expresiones Regulares. Si no sabes lo que son, Mozilla tiene un artículo muy bueno (practica con RegExr, es genial!). En Python funcionan de otro modo. Para más avanzados, aquí.

  • Descubre la 🧙‍ magia 🧙‍ dentro de Jupyter. Explora sus magics, como %time, %timeit o %matplotlib aquí.

    Fíjate! También son decoradores 🤩

  • Cómo funciona la Lectura/Escritura de archivos. Más aquí.

  • Este artículo repasa algunos conceptos ya vistos de manera más profunda.

  • Para los usuarios de Windows, recomiendo mirar el WSL o Windows Subsystem for Linux, el cual permite ejecutar sistemas Linux como Ubuntu o Debian dentro de Windows (al más puro estilo Docker).

  • Ve investigando alguna de las librerias que permiten aplicar modelos de Reinforcement Learning, como OpenAI gym, IntelAI coach, Unity ML Agents o PARL.

  • También puedes revisar librerias que te facilitan la creación y mejora de modelos, como Neptune y Tensorboard.

¿Un consejo? Busca código donde se use lo que menciono; lo más útil es que puedas ver cuándo y por qué se usan las distintas features del lenguaje - y por qué no usarlas si no es necesario.

Plataformas para practicar

  • Codewars permite practicar tus conocimientos de programación entorno a varios problemas - todo problema puedes resolverlo en más de 20 lenguajes: Python, Haskell, Java, ...

  • Otras plataformas como Exercism o Leetcode se centran en tracks o cursos completos, que tratan temas como Algoritmia, problemas de decisión o búsqueda.

  • Incluso Google ofrece un curso de Python. De cara a los próximos capítulos, recomiendo este libro sobre Python y Data Science, totalmente gratuito de manera online.

De todas formas, ésto solo es un pequeño grano en el mar de recursos que hay. Te recomiendo que hagas una búsqueda en GitHub, o pruebes en PyPi.

Cosillas feas de Python

  • La cantidad de versiones: si acabas de llegar a Python y te encuentras con alrededor de 8 versiones, tu cabeza hace 🤯🤯 incluso peor, es cuando tienes que usar Python2 y Python3 a la vez, pues tienes que operar como si fuesen dos lenguajes distintos. Ésto también se acentua cuando trabajas por proyecto (menos mal que existe Anaconda y PyEnv 🥳)

  • La dificultad para hacer concurrencia (ésto es algo avanzado): Python tiene varias implementaciones, y CPython, la más usada, implementa una estructura, llamada GIL, que obliga a correr Python en un solo procesador.

  • Las implementaciones completamente hechas en Python tienden a ser lentas, comparadas con C / C++ o Rust.


¿Más dudas?

Puedes encontrarnos por


Licencia: Andrés Matesanz y Joaquín Terrasa, MalagaAI CC BY-NC 4.0 2020